Skip to content

记一次 Canvas 内引用了 OSS 域的图片标签绘制时出现的 CORS 错误问题

标签
云服务/阿里云
云服务/阿里云/OSS-对象存储
开发/前端
网络/跨域
网络/跨域/CORS
开发/标记语言/HTML
开发/标记语言/HTML/Canvas
字数
2576 字
阅读时间
11 分钟
文档版本
编辑者版本变更日期变更说明
Nekov1.0.02022-09-02创建

文档兼容性

起因

小音和我说她使用的 html2canvas 引用的图片会随机丢失,有时候能稳定复现,有时候又会正常,但是总是有图片会丢失。

调查浏览器的开发者工具的控制台的时候发现是图片相关的响应未正确按照 CORS 期望的设定返回 Access-Control-Allow-Origin: * 头部,触发了 CORS 策略错误导致。 而如果不使用 CORS 策略(即打开 html2canvas 配置选项中的 useCORS: true 选项)请求图片的话,会导致 canvas 变成不安全的 canvas,或者说是被污染的 canvas(参见:启用了 CORS 的图片 - HTML(超文本标记语言) | MDN),在这样的情况下,将不可使用 canvas 上下文 context 调用下列函数:

  • toDataURL()
  • toBlob()
  • getImageData()

这影响了我们需求的开发和实现。

解决过程

检查 CORS

我先去检查了 OSS 的设置,发现 OSS 已经在 UAT 环境下配置好了 CORS 策略,并且允许了所有域访问、使用所有方法、包含所有 Header。

于是我又通过命令行直接检查了相关资源的 OPTIONS 请求结果:

shell
curl -H "Origin: https://localhost:3333" -H 'Access-Control-Request-Method: GET' -X OPTIONS 'https://oss.domain/image_key.jpg' -v

发现服务器确实有好好返回期望的 Access-Control-Allow-Origin: * 头部

shell
< HTTP/1.1 200 OK
< Server: AliyunOSS
< Date: Fri, 02 Sep 2022 08:57:21 GMT
< Content-Length: 0
< Connection: keep-alive
< x-oss-request-id: 6311C571DC44E0353601CA74
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, HEAD
< Access-Control-Max-Age: 0
< x-oss-server-time: 0

所以能排除部分 OSS 的问题了。

走弯路

在网络上搜索相关资料的时候发现大多数人都提到了一个之前我们开发时忽略的 <img> 属性 crossorigin(参考资料:HTMLImageElement.crossOrigin - Web APIs | MDN)。

由于我一开始没有正确理解 html2canvas 的工作原理,导致在这里我期望能够通过给 DOM 树中的 <img> 标签添加 crossorigin="anonymous" 属性配置来解决问题,但实际上问题根源不在这里,添加了属性配置也无济于事。 实际的情况是,由 <img> 标签参与的 DOM 树需要预先渲染一遍到浏览器中,然后再被 html2canvas 库读取到内部,再去使用独属于 <canvas> 标签上下文的处理逻辑来处理图片,此时此刻图片又会再次被通过 XHR 请求读取一次,而就像先前提到的 MDN 文档中所描述的:<canvas> 标签的安全要求远比 <img> 高得多,如果此时此刻出现了 CORS 错误,那么 canvas 的图片将会丢失,无法被渲染出来。对应的,也会影响到 toDataURL() 的调用。

所以我们不断去删除属性,开关浏览器开发者工具中网络选项卡的「停用缓存」选项来尝试稳定复现该问题,甚至一度怀疑是 Chrome 的缓存策略写的有 Bug 导致的。不过我们还是想的太简单,这个问题不仅在 Chrome 上能够出现,在首次打开开发环境网站的 Safari 中也有出现,观察请求序列和记录的时候能发现,部分的图片资源请求有时候会有 Access-Control-Allow-Origin: * 头存在,有时候又会没有,这个问题在图片资源来源为内存缓存、网络、磁盘缓存的请求中都能找到,而且结果经常反复闪烁,有时候有有时候无,但是如果一旦打开「停用缓存」选项,这个问题又会消失。

这个问题一度陷入无法解决的僵局之中。

我后来又找到两篇内容讲述了类似的遭遇,分别是:

他们都或多或少提到了:

It works well whether crossorigin="anonymous" or not, but sometimes it works, sometimes not either.

亦或是

If I add the crossOrigin="anonymous" I get the CORS error. If I leave that out, the images display but then the html2canvas plugin throws a CORS error as well when trying to generate the "screenshot".

这些情况都太过相似,我甚至一度以为就是 OSS 的服务端缓存实现问题导致了这个问题。 而他们的解决方案都是:在请求资源 URL 中添加一段随机字符串作为 query 参数进行传递。如果从已经解决了问题的我们的视角来看,这样的方法也确实能够解决缓存问题,只不过是从请求的资源 URL 上下手罢了,但是不太优雅,也许我们应该再探究探究问题的原因所在。

小音在这个时候提了一个问题:

如果是 CDN 或者是服务端请求导致的缓存滞后或是延迟问题,为什么同一个资源有时候有 CORS 错误,有时候无 CORS 错误呢?

这个问题其实我也不太清楚,也就是在这个时候我才坚定下来这是浏览器侧的缓存错误导致的问题,可是,如何修复呢?

偶然的一瞥

先前的问题还是没有解决,于是我去继续寻找其他问题的答案。有另一个问题是小音询问我的:<img> 标签能够加 crossorigin 属性来强制 CORS,那 CSS 里面的图片怎么办?。说实话我也不知道这个问题的答案,于是在网络上搜索。也就是在这个时候,我在 cross domain - Using CORS headers with CSS background-image - Stack Overflow StackOverflow 问答上见到了一句评论:

After doing some research I have determined that the real problem is that Amazon S3 doesn't set the "Vary: Origin" header even if it is configured to support CORS. If anyone knows how to get S3 to do that, that would also be a good answer. – Thayne Feb 11, 2014 at 16:17

意思是说:

在做了一些研究之后,我确定真正的问题是即使将 Amazon S3 配置为支持 CORS 的情况下,Amazon S3 依然没有自动设置 "Vary:Origin "头。如果有人知道如何让S3做到这一点,那也将是一个很好的答案。

咦,这个 Vary: Origin 看起来非常眼熟,之前在 OSS 的跨域配置中看到过,但是从未仔细了解过具体的含义。但是我们能在另一个回答中能找到些许线索:

Yes. If a request may contain a Access-Control-Allow-Origin with different values, then the CDN should always respond with Vary: Origin, even for responses without an Access-Control-Allow-Origin header. Your analysis is correct: if the header isn't always present, it would be possible to fill the cache with incorrect values. – monsuranswered Aug 15, 2014 at 16:02

其含义是:

是的,如果一个请求可能包含有不同值的 Access-Control-Allow-Origin,即使请求没有包含 Access-Control-Allow-Origin 头的响应,那么 CDN 应该总是包含返回 Vary:Origin 头部,你的分析是正确的:如果头信息不总是存在,就有可能用不正确的值填充缓存。

此时此刻我们去寻找 MDN 关于 Vary 头部的文档 Vary - HTTP | MDN,就能看到:

Vary 是一个 HTTP 响应头部信息,它决定了对于未来的一个请求头,应该用一个缓存的回复 (response) 还是向源服务器请求一个新的回复。

这句话可能不太能说明问题,我们继续看 MDN 提供的下面的例子:

哪种情况下使用 Vary?对于 User-Agent 头部信息,例如你提供给移动端的内容是不同的,可用防止你客户端误使用了用于桌面端的缓存。 并可帮助 Google 和其他搜索引擎来发现你的移动端版本的页面,同时告知他们不需要 Cloaking

即此时需要设置为 Vary: User-Agent

—— 来源 动态服务

也就是说如果我们需要根据 User-Agent 的不同指挥浏览器去读写缓存,那么我们就使用 Vary: User-Agent 头来实现。

此刻我们再去看看阿里云 OSS 的跨域设置选项当中的最后一项下面的注释:返回 Vary: Origin

关键的信息是:如果浏览器同时存在 CORS 和非 CORS 请求,请启用该选项否则会出现跨域问题。

综上所述,对于在跨域场景下的 GET 请求而言,我们能知道由于 CORS 策略所产生的的 OPTIONS 预检请求相比非 CORS 策略限制的的 GET 请求多了 Origin: <请求来源域名> 头部,那么先前在 cross domain - Using CORS headers with CSS background-image - Stack Overflow StackOverflow 问答中看到的评论所提到的 Vary: Origin 头部的作用就可以理解为:

根据不同的 Origin 值采用不同的缓存。

而 CORS 是会指定 Origin 头的,非 CORS 不会,那么此刻这两种看似相似的请求就能够被浏览器区分了,浏览器将能以此够决断对于未来的一个请求头,应该用一个缓存的响应还是向源服务器请求一个新的响应。此处的答案就是:它能知道应该采用 CORS 的响应而不是非 CORS 的响应

如果理论正确,浏览器中的缓存错乱问题就能被解决了。(找答案找的歪打正着了属于是)

修复

在阿里云 OSS 的 Bucket 后台中选择「权限设置」-「跨域设置」-「跨域设置」-「编辑」-「勾选 返回 Vary: Origin」。

勾选后重新刷新浏览器缓存,再次尝试复现先前的错误,能够发现问题已经被解决了。

贡献者

页面历史

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。